Skip to content

Conversation

@juherr
Copy link
Contributor

@juherr juherr commented Oct 19, 2025

@florinmandache I built on your work and made the minimal required changes.
Some tests would be very welcome. I also noticed that CSMS security requests aren’t implemented yet.

@goekay It seems to be working (at least it starts correctly), but I haven’t fully tested it yet and it could use some polishing.
Feel free to rework or improve it as you see fit.

Fix #100

@juherr juherr force-pushed the feature/ocpp16-security branch from e9b8666 to 81ba853 Compare October 19, 2025 11:28
@goekay
Copy link
Member

goekay commented Oct 20, 2025

@juherr i will take a look at this ASAP when i am finished with some other duties.

on a more general note: i had a conversation with @florinmandache after his initial big PR. he said that he has many things going on and is short on time, and he wanted to contribute on good will. since he is a busy person, he was not interested in follow-up, review rounds and eventual clean-ups (i.e. the usual process of PR and reviewing). due to the substantial nature of the contribution, i accepted it as-is and took it upon me to do whatever is necessary to bring these features to main branch.

in this context, i thank you for doing one part of it.

@juherr
Copy link
Contributor Author

juherr commented Oct 20, 2025

@goekay Thanks for the clarification. I think Gemini did an interesting initial job exploring the different topics, but it definitely needs more work based on what I’ve seen so far (especially regarding OCPP 1.6 Security and OICP). Since OCPI and OCPP 2.x are an even tougher challenge, and my time is just as limited, I’ll let you take the first shot on those ones 😉

# Conflicts:
#	src/main/java/de/rwth/idsg/steve/service/CentralSystemService16_Service.java
#	src/test/java/de/rwth/idsg/steve/utils/__DatabasePreparer__.java
@goekay
Copy link
Member

goekay commented Oct 31, 2025

FYI @juherr: steve-community/ocpp-jaxb#18

naming of the json schema files differ a bit from the original (see links from comment). do you know why?

@juherr
Copy link
Contributor Author

juherr commented Oct 31, 2025

Ah, you mean the request suffix in some filenames?
I personally find it cleaner and more robust to change it, but your approach seems to work fine too 👍

* clean up resources folder and pom file
* and do necessary changes for project to compile
@goekay
Copy link
Member

goekay commented Nov 2, 2025

i did some "low-hanging fruit" refactoring to reduce the size of the PR (i.e. moving the data models and their generation to ocpp-jaxb library). will do some more. i think this is very important in order to able to properly review the PR and separate the good parts from inaccurate AI slop. while being on this...

i did a litmus test and tried to understand how "http basic auth" (security profile 1) is supposed to work in the impl. ChargePointRepository has 2 new methods isRegistered(String chargeBoxId) and validatePassword(String chargeBoxId, String password), but no one is using them.

moreover, there is no change in SecurityConfiguration or OcppWebSocketHandshakeHandler (more emphasis on this), such that the handler retrieves the basic auth header from request and validates the password against the DB.

* and also make getRegistrationStatus return the password additionally, in order prevent another DB lookup later
@goekay goekay force-pushed the feature/ocpp16-security branch from 036e217 to 3878731 Compare November 3, 2025 09:30
@goekay
Copy link
Member

goekay commented Nov 4, 2025

WIP - Implementation status of new operations

Operation Initiator Request Handling Response Handling
CertificateSigned Central System ? ?
DeleteCertificate Central System ? ?
ExtendedTriggerMessage Central System
GetInstalledCertificateIds Central System ? ?
GetLog Central System ? ?
InstallCertificate Central System ? ?
LogStatusNotification Charge Point
SecurityEventNotification Charge Point
SignCertificate Charge Point ? ?
SignedFirmwareStatusNotification Charge Point
SignedUpdateFirmware Central System ? ?

@goekay
Copy link
Member

goekay commented Nov 5, 2025

ignoring and removing the following method completely, since it has no correlation with reality IMO:

    private static String determineSeverity(String eventType) {
        if (eventType == null) {
            return "INFO";
        }

        var upperType = eventType.toUpperCase();

        if (upperType.contains("ATTACK") || upperType.contains("BREACH") || upperType.contains("TAMPER")) {
            return "CRITICAL";
        }

        if (upperType.contains("FAIL") || upperType.contains("ERROR") || upperType.contains("INVALID")
            || upperType.contains("UNAUTHORIZED") || upperType.contains("REJECT")) {
            return "HIGH";
        }

        if (upperType.contains("WARNING") || upperType.contains("EXPIR")) {
            return "MEDIUM";
        }

        return "INFO";
    }

the eventType references type field in SecurityEventNotification message with the following value possibilities:

Screenshot 2025-11-05 at 13 49 44

the table actually continues in the next page with more types, but there is no type with INFO, ATTACK (heh?), BREACH (what?) etc. so this whole thing is made up.

moreover, the PR invents new types (that are not in the list), such as SignCertificateError with a HIGH severity, for ex in

            securityRepository.insertSecurityEvent(
                chargeBoxIdentity,
                "SignCertificateError",
                DateTime.now(),
                "Error signing CSR: " + e.getMessage(),
                "HIGH"
            );

these usages feel also wrong. actually, the motivation is nice: if there is an error during the signing of certificate, create a security event and store it in DB. but, these security events are NOT coming from station, and intermixing these with regular events is problematic IMO.

@juherr do you happen to know whether these are retrofits coming from ocpp 2.x ?

@juherr
Copy link
Contributor Author

juherr commented Nov 5, 2025

@juherr do you happen to know whether these are retrofits coming from ocpp 2.x ?

I haven’t looked deeply into OCPP 2.x or even the current PR once I understood where it originated from.
My assumption is that Gemini applied a pattern from another context by mistake.

According to the whitepaper, only critical events are supposed to be sent by the station — and I believe all of those should indeed be stored.
Happy to help if you can elaborate a bit more on your question.

@goekay
Copy link
Member

goekay commented Nov 5, 2025

so, i can assume that these additions were a mistake and i can remove them

  • additional severity column derived from security event type
  • additional security event inserts (triggered by backend/steve) when local certificate-related operation fails

?

* various simplifications and improvements
* certificate is still as is. this is a future TODO
@goekay goekay force-pushed the feature/ocpp16-security branch from db70668 to 59c1c8d Compare November 5, 2025 15:12
@juherr
Copy link
Contributor Author

juherr commented Nov 5, 2025

In my opinion, security events triggered by the backend are a separate concern, and a warning log should be enough for now.
At the very least, an internal event could be raised so that any fork can decide how it wants to handle it.

@goekay goekay force-pushed the feature/ocpp16-security branch from d54af17 to d84e47c Compare November 5, 2025 17:43
Copy link
Contributor Author

@juherr juherr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some remarks after a first review of the changes.
I should be able to make them by myself as it comes from my branch, but I would like your review first.

// -------------------------------------------------------------------------

public void extendedTriggerMessage(ChargePointSelect cp, ExtendedTriggerMessageTask task) {
if (cp.isJson()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to throw an exception when the CP isn’t JSON, rather than silently ignoring the case. This would make misconfigurations easier to detect.

Comment on lines +313 to +315
var signedCertificatePem = certificateSigningService.signCertificateRequest(csr, chargeBoxIdentity);
var caCertificatePem = certificateSigningService.getCertificateChain();
var certificateChain = signedCertificatePem + caCertificatePem;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be grouped into a single method in the service that returns the signedCertificatePem and the certificateChain length

Comment on lines +385 to +395
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertLogUploadStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertLogUploadStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
if (parameters.getRequestId() == null) {
throw new InvalidArgumentException("RequestId is null");
}
try {
securityRepository.insertLogUploadStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}

Comment on lines +365 to +375
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertFirmwareUpdateStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
try {
if (parameters.getRequestId() == null) {
log.warn("No requestId in {}", parameters);
} else {
securityRepository.insertFirmwareUpdateStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}
if (parameters.getRequestId() == null) {
throw new InvalidArgumentException("RequestId is null");
}
try {
securityRepository.insertFirmwareUpdateStatus(
chargeBoxIdentity,
parameters.getRequestId(),
parameters.getStatus().value(),
DateTime.now()
);
}

Comment on lines +85 to +94
String keystorePath = securityConfig.getKeystorePath();
String keystorePassword = securityConfig.getKeystorePassword();
String keystoreType = securityConfig.getKeystoreType();

KeyStore keystore = KeyStore.getInstance(keystoreType);
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keystore.load(fis, keystorePassword.toCharArray());
}

String alias = keystore.aliases().nextElement();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
String keystorePath = securityConfig.getKeystorePath();
String keystorePassword = securityConfig.getKeystorePassword();
String keystoreType = securityConfig.getKeystoreType();
KeyStore keystore = KeyStore.getInstance(keystoreType);
try (FileInputStream fis = new FileInputStream(keystorePath)) {
keystore.load(fis, keystorePassword.toCharArray());
}
String alias = keystore.aliases().nextElement();
var keystorePath = securityConfig.getKeystorePath();
var keystorePassword = securityConfig.getKeystorePassword();
var keystoreType = securityConfig.getKeystoreType();
var keystore = KeyStore.getInstance(keystoreType);
try (var fis = new FileInputStream(keystorePath)) {
keystore.load(fis, keystorePassword.toCharArray());
}
var alias = keystore.aliases().nextElement();

Comment on lines +34 to +36
@Size(max = 10000, message = "Certificate chain must not exceed {max} characters")
@Schema(description = "PEM-encoded certificate chain (signed certificate + CA certificate)",
requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 10000)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Size(max = 10000, message = "Certificate chain must not exceed {max} characters")
@Schema(description = "PEM-encoded certificate chain (signed certificate + CA certificate)",
requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 10000)
@Size(max = 10_000, message = "Certificate chain must not exceed {max} characters")
@Schema(description = "PEM-encoded certificate chain (signed certificate + CA certificate)",
requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 10_000)

Comment on lines +39 to +41
@Size(max = 5500, message = "Certificate must not exceed {max} characters")
@Schema(description = "PEM-encoded X.509 certificate", requiredMode = Schema.RequiredMode.REQUIRED,
maxLength = 5500)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Size(max = 5500, message = "Certificate must not exceed {max} characters")
@Schema(description = "PEM-encoded X.509 certificate", requiredMode = Schema.RequiredMode.REQUIRED,
maxLength = 5500)
@Size(max = 5_500, message = "Certificate must not exceed {max} characters")
@Schema(description = "PEM-encoded X.509 certificate", requiredMode = Schema.RequiredMode.REQUIRED,
maxLength = 5_500)

Comment on lines +50 to +56
@Size(max = 5000, message = "Firmware signature must not exceed {max} characters")
@Schema(description = "Cryptographic signature of the firmware file",
requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 5000)
private String firmwareSignature;

@Size(max = 5500, message = "Signing certificate must not exceed {max} characters")
@Schema(description = "PEM-encoded certificate used to sign the firmware", maxLength = 5500)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Size(max = 5000, message = "Firmware signature must not exceed {max} characters")
@Schema(description = "Cryptographic signature of the firmware file",
requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 5000)
private String firmwareSignature;
@Size(max = 5500, message = "Signing certificate must not exceed {max} characters")
@Schema(description = "PEM-encoded certificate used to sign the firmware", maxLength = 5500)
@Size(max = 5_000, message = "Firmware signature must not exceed {max} characters")
@Schema(description = "Cryptographic signature of the firmware file",
requiredMode = Schema.RequiredMode.REQUIRED, maxLength = 5_000)
private String firmwareSignature;
@Size(max = 5_500, message = "Signing certificate must not exceed {max} characters")
@Schema(description = "PEM-encoded certificate used to sign the firmware", maxLength = 5_500)

Comment on lines +79 to +91
<td>
<c:choose>
<c:when test="${event.severity == 'CRITICAL' || event.severity == 'HIGH'}">
<span style="color: red; font-weight: bold;">${event.severity}</span>
</c:when>
<c:when test="${event.severity == 'MEDIUM'}">
<span style="color: orange; font-weight: bold;">${event.severity}</span>
</c:when>
<c:otherwise>
${event.severity}
</c:otherwise>
</c:choose>
</td>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the latest discussion, it should be removed as there is no longer severity

Comment on lines +45 to +50
<select name="limit">
<option value="50" ${limit == 50 ? 'selected' : ''}>50</option>
<option value="100" ${limit == 100 ? 'selected' : ''}>100</option>
<option value="250" ${limit == 250 ? 'selected' : ''}>250</option>
<option value="500" ${limit == 500 ? 'selected' : ''}>500</option>
</select>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limit is great if available in all pages. As I remember, it is not always the case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OCPP 1.6-J Security

3 participants